]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
calendar-server: Improve performance by properly using ECalClientView
authorMilan Crha <mcrha@redhat.com>
Thu, 2 Apr 2020 07:24:28 +0000 (09:24 +0200)
committerGeorges Basile Stavracas Neto <georges.stavracas@gmail.com>
Mon, 27 Apr 2020 16:14:57 +0000 (16:14 +0000)
The previous code always restarted whole ECalClientView when it received
any changes in it, which could sometimes lead to constant repeated restarts
of the view.

https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/1875

data/dbus-interfaces/org.gnome.Shell.CalendarServer.xml
js/ui/calendar.js
meson.build
src/calendar-server/calendar-sources.c
src/calendar-server/calendar-sources.h
src/calendar-server/gnome-shell-calendar-server.c

index c19883095e61f4d92f26402708c0c17d724fe31b..51b71ef03e1d81b188dd5f0795eef097790b1312 100644 (file)
@@ -1,12 +1,19 @@
 <node>
   <interface name="org.gnome.Shell.CalendarServer">
-    <method name="GetEvents">
-      <arg type="x" direction="in" />
-      <arg type="x" direction="in" />
-      <arg type="b" direction="in" />
-      <arg type="a(sssbxxa{sv})" direction="out" />
+    <method name="SetTimeRange">
+      <arg type="x" name="since" direction="in"/>
+      <arg type="x" name="until" direction="in"/>
+      <arg type="b" name="force_reload" direction="in"/>
     </method>
+    <signal name="EventsAddedOrUpdated">
+      <arg type="a(ssbxxa{sv})" name="events" direction="out"/>
+    </signal>
+    <signal name="EventsRemoved">
+      <arg type="as" name="ids" direction="out"/>
+    </signal>
+    <signal name="ClientDisappeared">
+      <arg type="s" name="source_uid" direction="out"/>
+    </signal>
     <property name="HasCalendars" type="b" access="read" />
-    <signal name="Changed" />
   </interface>
 </node>
index 94982cf882ddac61d4e2ff0289d030a61e02b795..1a25dde256c86d0f737f6aa09aaf4e91167bcddc 100644 (file)
@@ -222,7 +222,12 @@ class DBusEventSource extends EventSourceBase {
             }
         }
 
-        this._dbusProxy.connectSignal('Changed', this._onChanged.bind(this));
+        this._dbusProxy.connectSignal('EventsAddedOrUpdated',
+            this._onEventsAddedOrUpdated.bind(this));
+        this._dbusProxy.connectSignal('EventsRemoved',
+            this._onEventsRemoved.bind(this));
+        this._dbusProxy.connectSignal('ClientDisappeared',
+            this._onClientDisappeared.bind(this));
 
         this._dbusProxy.connect('notify::g-name-owner', () => {
             if (this._dbusProxy.g_name_owner)
@@ -258,7 +263,7 @@ class DBusEventSource extends EventSourceBase {
     }
 
     _resetCache() {
-        this._events = [];
+        this._events = new Map();
         this._lastRequestBegin = null;
         this._lastRequestEnd = null;
     }
@@ -274,28 +279,47 @@ class DBusEventSource extends EventSourceBase {
         this.emit('changed');
     }
 
-    _onChanged() {
-        this._loadEvents(false);
-    }
+    _onEventsAddedOrUpdated(dbusProxy, nameOwner, argArray) {
+        const [appointments = []] = argArray;
+        let changed = false;
 
-    _onEventsReceived(results, _error) {
-        let newEvents = [];
-        let appointments = results[0] || [];
         for (let n = 0; n < appointments.length; n++) {
-            let a = appointments[n];
-            let date = new Date(a[4] * 1000);
-            let end = new Date(a[5] * 1000);
-            let id = a[0];
-            let summary = a[1];
-            let allDay = a[3];
+            const [id, summary, allDay, startTime, endTime] = appointments[n];
+            const date = new Date(startTime * 1000);
+            const end = new Date(endTime * 1000);
             let event = new CalendarEvent(id, date, end, summary, allDay);
-            newEvents.push(event);
+            this._events.set(event.id, event);
+
+            changed = true;
         }
-        newEvents.sort((ev1, ev2) => ev1.date.getTime() - ev2.date.getTime());
 
-        this._events = newEvents;
-        this._isLoading = false;
-        this.emit('changed');
+        if (changed)
+            this.emit('changed');
+    }
+
+    _onEventsRemoved(dbusProxy, nameOwner, argArray) {
+        const [ids = []] = argArray;
+
+        let changed = false;
+        for (const id of ids)
+            changed |= this._events.delete(id);
+
+        if (changed)
+            this.emit('changed');
+    }
+
+    _onClientDisappeared(dbusProxy, nameOwner, argArray) {
+        let [sourceUid = ''] = argArray;
+        sourceUid += '\n';
+
+        let changed = false;
+        for (const id of this._events.keys()) {
+            if (id.startsWith(sourceUid))
+                changed |= this._events.delete(id);
+        }
+
+        if (changed)
+            this.emit('changed');
     }
 
     _loadEvents(forceReload) {
@@ -304,27 +328,30 @@ class DBusEventSource extends EventSourceBase {
             return;
 
         if (this._curRequestBegin && this._curRequestEnd) {
-            this._dbusProxy.GetEventsRemote(this._curRequestBegin.getTime() / 1000,
-                                            this._curRequestEnd.getTime() / 1000,
-                                            forceReload,
-                                            this._onEventsReceived.bind(this),
-                                            Gio.DBusCallFlags.NONE);
+            if (forceReload) {
+                this._events.clear();
+                this.emit('changed');
+            }
+            this._dbusProxy.SetTimeRangeRemote(
+                this._curRequestBegin.getTime() / 1000,
+                this._curRequestEnd.getTime() / 1000,
+                forceReload,
+                Gio.DBusCallFlags.NONE);
         }
     }
 
     requestRange(begin, end) {
         if (!(_datesEqual(begin, this._lastRequestBegin) && _datesEqual(end, this._lastRequestEnd))) {
-            this._isLoading = true;
             this._lastRequestBegin = begin;
             this._lastRequestEnd = end;
             this._curRequestBegin = begin;
             this._curRequestEnd = end;
-            this._loadEvents(false);
+            this._loadEvents(true);
         }
     }
 
     *_getFilteredEvents(begin, end) {
-        for (const event of this._events) {
+        for (const event of this._events.values()) {
             if (_dateIntervalsOverlap(event.date, event.end, begin, end))
                 yield event;
         }
@@ -879,7 +906,7 @@ class EventsSection extends MessageList.MessageListSection {
     }
 
     _reloadEvents() {
-        if (this._eventSource.isLoading)
+        if (this._eventSource.isLoading || this._reloading)
             return;
 
         this._reloading = true;
index 95a6868f35dd9c74ba32609914bd6b59b0084afc..3dacbaa2db51c6b1727fd06aae70f6b46f063bd7 100644 (file)
@@ -19,7 +19,7 @@ cogl_pango_pc = 'mutter-cogl-pango-' + mutter_api_version
 libmutter_pc = 'libmutter-' + mutter_api_version
 
 ecal_req = '>= 3.33.1'
-eds_req = '>= 3.17.2'
+eds_req = '>= 3.33.1'
 gcr_req = '>= 3.7.5'
 gio_req = '>= 2.56.0'
 gi_req = '>= 1.49.1'
index 3281500ec1662c5e9d40e940123af6db324b3be3..d09956831ef2072a224578a34426fe741ee968ad 100644 (file)
@@ -45,137 +45,122 @@ struct _ClientData
   gulong backend_died_id;
 };
 
-struct _CalendarSourceData
-{
-  ECalClientSourceType source_type;
-  CalendarSources *sources;
-  guint            changed_signal;
-
-  /* ESource -> EClient */
-  GHashTable      *clients;
-
-  guint            timeout_id;
-
-  guint            loaded : 1;
-};
-
 typedef struct _CalendarSourcesPrivate CalendarSourcesPrivate;
 
 struct _CalendarSources
 {
   GObject                 parent;
 
-  ESourceRegistry    *registry;
-  gulong              source_added_id;
-  gulong              source_changed_id;
-  gulong              source_removed_id;
+  ESourceRegistryWatcher *registry_watcher;
+  gulong                  filter_id;
+  gulong                  appeared_id;
+  gulong                  disappeared_id;
 
-  CalendarSourceData  appointment_sources;
-  CalendarSourceData  task_sources;
+  GMutex                  clients_lock;
+  GHashTable             *clients; /* ESource -> ClientData */
 };
 
 G_DEFINE_TYPE (CalendarSources, calendar_sources, G_TYPE_OBJECT)
 
-static void calendar_sources_finalize   (GObject             *object);
-
-static void backend_died_cb (EClient *client, CalendarSourceData *source_data);
-static void calendar_sources_registry_source_changed_cb (ESourceRegistry *registry,
-                                                         ESource         *source,
-                                                         CalendarSources *sources);
-static void calendar_sources_registry_source_removed_cb (ESourceRegistry *registry,
-                                                         ESource         *source,
-                                                         CalendarSources *sources);
-
 enum
 {
-  APPOINTMENT_SOURCES_CHANGED,
-  TASK_SOURCES_CHANGED,
+  CLIENT_APPEARED,
+  CLIENT_DISAPPEARED,
   LAST_SIGNAL
 };
 static guint signals [LAST_SIGNAL] = { 0, };
 
-static GObjectClass    *parent_class = NULL;
-static CalendarSources *calendar_sources_singleton = NULL;
+static void
+calendar_sources_client_connected_cb (GObject *source_object,
+                                      GAsyncResult *result,
+                                      gpointer user_data)
+{
+  CalendarSources *sources = CALENDAR_SOURCES (source_object);
+  ESource *source = user_data;
+  EClient *client;
+  g_autoptr (GError) error = NULL;
+
+  /* The calendar_sources_connect_client_sync() already stored the 'client'
+   * into the sources->clients */
+  client = calendar_sources_connect_client_finish (sources, result, &error);
+  if (error)
+    {
+      g_warning ("Could not load source '%s': %s",
+                 e_source_get_uid (source),
+                 error->message);
+    }
+   else
+    {
+      g_signal_emit (sources, signals[CLIENT_APPEARED], 0, client, NULL);
+    }
+
+  g_clear_object (&client);
+  g_clear_object (&source);
+}
+
+static gboolean
+registry_watcher_filter_cb (ESourceRegistryWatcher *watcher,
+                            ESource *source,
+                            CalendarSources *sources)
+{
+  return e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR) &&
+         e_source_selectable_get_selected (e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR));
+}
 
 static void
-client_data_free (ClientData *data)
+registry_watcher_source_appeared_cb (ESourceRegistryWatcher *watcher,
+                                     ESource *source,
+                                     CalendarSources *sources)
 {
-  g_clear_signal_handler (&data->backend_died_id, data->client);
-  g_object_unref (data->client);
-  g_slice_free (ClientData, data);
+  ECalClientSourceType source_type;
+
+  if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR))
+    source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS;
+  else if (e_source_has_extension (source, E_SOURCE_EXTENSION_MEMO_LIST))
+    source_type = E_CAL_CLIENT_SOURCE_TYPE_MEMOS;
+  else if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST))
+    source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS;
+  else
+    g_return_if_reached ();
+
+  calendar_sources_connect_client (sources, source, source_type, 30, NULL, calendar_sources_client_connected_cb, g_object_ref (source));
 }
 
 static void
-calendar_sources_class_init (CalendarSourcesClass *klass)
+registry_watcher_source_disappeared_cb (ESourceRegistryWatcher *watcher,
+                                        ESource *source,
+                                        CalendarSources *sources)
 {
-  GObjectClass *gobject_class = (GObjectClass *) klass;
+  gboolean emit;
 
-  parent_class = g_type_class_peek_parent (klass);
+  g_mutex_lock (&sources->clients_lock);
 
-  gobject_class->finalize = calendar_sources_finalize;
+  emit = g_hash_table_remove (sources->clients, source);
 
-  signals [APPOINTMENT_SOURCES_CHANGED] =
-    g_signal_new ("appointment-sources-changed",
-                  G_TYPE_FROM_CLASS (gobject_class),
-                  G_SIGNAL_RUN_LAST,
-                  0,
-                  NULL,
-                  NULL,
-                  NULL,
-                  G_TYPE_NONE,
-                  0);
+  g_mutex_unlock (&sources->clients_lock);
 
-  signals [TASK_SOURCES_CHANGED] =
-    g_signal_new ("task-sources-changed",
-                  G_TYPE_FROM_CLASS (gobject_class),
-                  G_SIGNAL_RUN_LAST,
-                  0,
-                  NULL,
-                  NULL,
-                  NULL,
-                  G_TYPE_NONE,
-                  0);
+  if (emit)
+    g_signal_emit (sources, signals[CLIENT_DISAPPEARED], 0, e_source_get_uid (source), NULL);
 }
 
 static void
-calendar_sources_init (CalendarSources *sources)
+client_data_free (ClientData *data)
 {
+  g_signal_handler_disconnect (data->client, data->backend_died_id);
+  g_object_unref (data->client);
+  g_slice_free (ClientData, data);
+}
+
+static void
+calendar_sources_constructed (GObject *object)
+{
+  CalendarSources *sources = CALENDAR_SOURCES (object);
+  ESourceRegistry *registry = NULL;
   GError *error = NULL;
-  GDBusConnection *session_bus;
-  GVariant *result;
-
-  /* WORKAROUND: the hardcoded timeout for e_source_registry_new_sync()
-     (and other library calls that eventually call g_dbus_proxy_new[_sync]())
-     is 25 seconds. This has been shown to be too small for
-     evolution-source-registry in certain cases (slow disk, concurrent IO,
-     many configured sources), so we first ensure that the service
-     starts with a manual call and a higher timeout.
-
-     HACK: every time the DBus API is bumped in e-d-s we need
-     to update this!
-  */
-  session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
-  if (session_bus == NULL)
-    {
-      g_error ("Failed to connect to the session bus: %s", error->message);
-    }
 
-  result = g_dbus_connection_call_sync (session_bus, "org.freedesktop.DBus",
-                                        "/", "org.freedesktop.DBus",
-                                        "StartServiceByName",
-                                        g_variant_new ("(su)",
-                                                       "org.gnome.evolution.dataserver.Sources5",
-                                                       0),
-                                        NULL,
-                                        G_DBUS_CALL_FLAGS_NONE,
-                                        60 * 1000,
-                                        NULL, &error);
-  if (result != NULL)
-    {
-      g_variant_unref (result);
-      sources->registry = e_source_registry_new_sync (NULL, &error);
-    }
+  G_OBJECT_CLASS (calendar_sources_parent_class)->constructed (object);
 
+  registry = e_source_registry_new_sync (NULL, &error);
   if (error != NULL)
     {
       /* Any error is fatal, but we don't want to crash gnome-shell-calendar-server
@@ -185,82 +170,98 @@ calendar_sources_init (CalendarSources *sources)
       exit (EXIT_FAILURE);
     }
 
-  g_object_unref (session_bus);
-
-  sources->source_added_id = g_signal_connect (sources->registry,
-                                               "source-added",
-                                               G_CALLBACK (calendar_sources_registry_source_changed_cb),
-                                               sources);
-  sources->source_changed_id = g_signal_connect (sources->registry,
-                                                 "source-changed",
-                                                 G_CALLBACK (calendar_sources_registry_source_changed_cb),
-                                                 sources);
-  sources->source_removed_id = g_signal_connect (sources->registry,
-                                                 "source-removed",
-                                                 G_CALLBACK (calendar_sources_registry_source_removed_cb),
-                                                 sources);
-
-  sources->appointment_sources.source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS;
-  sources->appointment_sources.sources = sources;
-  sources->appointment_sources.changed_signal = signals [APPOINTMENT_SOURCES_CHANGED];
-  sources->appointment_sources.clients = g_hash_table_new_full ((GHashFunc) e_source_hash,
-                                                                (GEqualFunc) e_source_equal,
-                                                                (GDestroyNotify) g_object_unref,
-                                                                (GDestroyNotify) client_data_free);
-  sources->appointment_sources.timeout_id = 0;
-
-  sources->task_sources.source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS;
-  sources->task_sources.sources = sources;
-  sources->task_sources.changed_signal = signals [TASK_SOURCES_CHANGED];
-  sources->task_sources.clients = g_hash_table_new_full ((GHashFunc) e_source_hash,
-                                                         (GEqualFunc) e_source_equal,
-                                                         (GDestroyNotify) g_object_unref,
-                                                         (GDestroyNotify) client_data_free);
-  sources->task_sources.timeout_id = 0;
+  g_return_if_fail (registry != NULL);
+
+  sources->registry_watcher = e_source_registry_watcher_new (registry, NULL);
+
+  g_clear_object (&registry);
+
+  sources->clients = g_hash_table_new_full ((GHashFunc) e_source_hash,
+                                            (GEqualFunc) e_source_equal,
+                                            (GDestroyNotify) g_object_unref,
+                                            (GDestroyNotify) client_data_free);
+  sources->filter_id = g_signal_connect (sources->registry_watcher,
+                                         "filter",
+                                         G_CALLBACK (registry_watcher_filter_cb),
+                                         sources);
+  sources->appeared_id = g_signal_connect (sources->registry_watcher,
+                                           "appeared",
+                                           G_CALLBACK (registry_watcher_source_appeared_cb),
+                                           sources);
+  sources->disappeared_id = g_signal_connect (sources->registry_watcher,
+                                              "disappeared",
+                                              G_CALLBACK (registry_watcher_source_disappeared_cb),
+                                              sources);
+
+  e_source_registry_watcher_reclaim (sources->registry_watcher);
 }
 
 static void
-calendar_sources_finalize_source_data (CalendarSources    *sources,
-                                       CalendarSourceData *source_data)
+calendar_sources_finalize (GObject *object)
 {
-  if (source_data->loaded)
-    {
-      g_hash_table_destroy (source_data->clients);
-      source_data->clients = NULL;
+  CalendarSources *sources = CALENDAR_SOURCES (object);
 
-      g_clear_handle_id (&source_data->timeout_id, g_source_remove);
+  g_clear_pointer (&sources->clients, g_hash_table_destroy);
 
-      source_data->loaded = FALSE;
+  if (sources->registry_watcher)
+    {
+      g_signal_handler_disconnect (sources->registry_watcher,
+                                   sources->filter_id);
+      g_signal_handler_disconnect (sources->registry_watcher,
+                                   sources->appeared_id);
+      g_signal_handler_disconnect (sources->registry_watcher,
+                                   sources->disappeared_id);
+      g_clear_object (&sources->registry_watcher);
     }
+
+  g_mutex_clear (&sources->clients_lock);
+
+  G_OBJECT_CLASS (calendar_sources_parent_class)->finalize (object);
 }
 
 static void
-calendar_sources_finalize (GObject *object)
+calendar_sources_class_init (CalendarSourcesClass *klass)
 {
-  CalendarSources *sources = CALENDAR_SOURCES (object);
+  GObjectClass *gobject_class = (GObjectClass *) klass;
 
-  if (sources->registry)
-    {
-      g_clear_signal_handler (&sources->source_added_id,
-                              sources->registry);
-      g_clear_signal_handler (&sources->source_changed_id,
-                              sources->registry);
-      g_clear_signal_handler (&sources->source_removed_id,
-                              sources->registry);
-      g_object_unref (sources->registry);
-    }
-  sources->registry = NULL;
+  gobject_class->constructed = calendar_sources_constructed;
+  gobject_class->finalize = calendar_sources_finalize;
+
+  signals [CLIENT_APPEARED] =
+    g_signal_new ("client-appeared",
+                  G_TYPE_FROM_CLASS (gobject_class),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  1,
+                  E_TYPE_CAL_CLIENT);
 
-  calendar_sources_finalize_source_data (sources, &sources->appointment_sources);
-  calendar_sources_finalize_source_data (sources, &sources->task_sources);
+  signals [CLIENT_DISAPPEARED] =
+    g_signal_new ("client-disappeared",
+                  G_TYPE_FROM_CLASS (gobject_class),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  1,
+                  G_TYPE_STRING); /* ESource::uid of the disappeared client */
+}
 
-  if (G_OBJECT_CLASS (parent_class)->finalize)
-    G_OBJECT_CLASS (parent_class)->finalize (object);
+static void
+calendar_sources_init (CalendarSources *sources)
+{
+  g_mutex_init (&sources->clients_lock);
 }
 
 CalendarSources *
 calendar_sources_get (void)
 {
+  static CalendarSources *calendar_sources_singleton = NULL;
   gpointer singleton_location = &calendar_sources_singleton;
 
   if (calendar_sources_singleton)
@@ -273,80 +274,65 @@ calendar_sources_get (void)
   return calendar_sources_singleton;
 }
 
-/* The clients are just created here but not loaded */
+ESourceRegistry *
+calendar_sources_get_registry (CalendarSources *sources)
+{
+  return e_source_registry_watcher_get_registry (sources->registry_watcher);
+}
+
 static void
-create_client_for_source (ESource              *source,
-                          ECalClientSourceType  source_type,
-                          CalendarSourceData   *source_data)
+gather_event_clients_cb (gpointer key,
+                         gpointer value,
+                         gpointer user_data)
 {
-  ClientData *data;
-  EClient *client;
-  GError *error = NULL;
+  GSList **plist = user_data;
+  ClientData *cd = value;
 
-  client = g_hash_table_lookup (source_data->clients, source);
-  g_return_if_fail (client == NULL);
+  if (cd)
+    *plist = g_slist_prepend (*plist, g_object_ref (cd->client));
+}
 
-  client = e_cal_client_connect_sync (source, source_type, -1, NULL, &error);
-  if (!client)
-    {
-      g_warning ("Could not load source '%s': %s",
-                 e_source_get_uid (source),
-                 error->message);
-      g_clear_error (&error);
-      return;
-    }
+GSList *
+calendar_sources_ref_clients (CalendarSources *sources)
+{
+  GSList *list = NULL;
 
-  data = g_slice_new0 (ClientData);
-  data->client = E_CAL_CLIENT (client);  /* takes ownership */
-  data->backend_died_id = g_signal_connect (client,
-                                            "backend-died",
-                                            G_CALLBACK (backend_died_cb),
-                                            source_data);
+  g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), NULL);
+
+  g_mutex_lock (&sources->clients_lock);
+  g_hash_table_foreach (sources->clients, gather_event_clients_cb, &list);
+  g_mutex_unlock (&sources->clients_lock);
 
-  g_hash_table_insert (source_data->clients, g_object_ref (source), data);
+  return list;
 }
 
-static inline void
-debug_dump_ecal_list (GHashTable *clients)
+gboolean
+calendar_sources_has_clients (CalendarSources *sources)
 {
-#ifdef CALENDAR_ENABLE_DEBUG
-  GList *list, *link;
+  GHashTableIter iter;
+  gpointer value;
+  gboolean has = FALSE;
 
-  dprintf ("Loaded clients:\n");
-  list = g_hash_table_get_keys (clients);
-  for (link = list; link != NULL; link = g_list_next (link))
-    {
-      ESource *source = E_SOURCE (link->data);
+  g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), FALSE);
 
-      dprintf ("  %s %s\n",
-               e_source_get_uid (source),
-               e_source_get_display_name (source));
-    }
-  g_list_free (list);
-#endif
-}
+  g_mutex_lock (&sources->clients_lock);
 
-static void
-calendar_sources_load_esource_list (ESourceRegistry *registry,
-                                    CalendarSourceData *source_data);
+  g_hash_table_iter_init (&iter, sources->clients);
+  while (!has && g_hash_table_iter_next (&iter, NULL, &value))
+   {
+     ClientData *cd = value;
 
-static gboolean
-backend_restart (gpointer data)
-{
-  CalendarSourceData *source_data = data;
-  ESourceRegistry *registry;
+     has = cd != NULL;
+   }
 
-  registry = source_data->sources->registry;
-  calendar_sources_load_esource_list (registry, source_data);
-  g_signal_emit (source_data->sources, source_data->changed_signal, 0);
+  g_mutex_unlock (&sources->clients_lock);
 
-  source_data->timeout_id = 0;
-    
-  return FALSE;
+  return has;
 }
 
 static void
-backend_died_cb (EClient *client, CalendarSourceData *source_data)
+backend_died_cb (EClient *client,
+                 CalendarSources *sources)
 {
   ESource *source;
   const char *display_name;
@@ -354,196 +340,167 @@ backend_died_cb (EClient *client, CalendarSourceData *source_data)
   source = e_client_get_source (client);
   display_name = e_source_get_display_name (source);
   g_warning ("The calendar backend for '%s' has crashed.", display_name);
-  g_hash_table_remove (source_data->clients, source);
-
-  g_clear_handle_id (&source_data->timeout_id, g_source_remove);
-
-  source_data->timeout_id = g_timeout_add_seconds (2, backend_restart,
-                                                   source_data);
-  g_source_set_name_by_id (source_data->timeout_id, "[gnome-shell] backend_restart");
+  g_mutex_lock (&sources->clients_lock);
+  g_hash_table_remove (sources->clients, source);
+  g_mutex_unlock (&sources->clients_lock);
 }
 
-static void
-calendar_sources_load_esource_list (ESourceRegistry *registry,
-                                    CalendarSourceData *source_data)
+static EClient *
+calendar_sources_connect_client_sync (CalendarSources *sources,
+                                      ESource *source,
+                                      ECalClientSourceType source_type,
+                                      guint32 wait_for_connected_seconds,
+                                      GCancellable *cancellable,
+                                      GError **error)
 {
-  GList   *list, *link;
-  const gchar *extension_name;
+  EClient *client = NULL;
+  ClientData *client_data;
 
-  switch (source_data->source_type)
-    {
-      case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
-        extension_name = E_SOURCE_EXTENSION_CALENDAR;
-        break;
-      case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
-        extension_name = E_SOURCE_EXTENSION_TASK_LIST;
-        break;
-      default:
-        g_return_if_reached ();
-    }
+  g_mutex_lock (&sources->clients_lock);
+  client_data = g_hash_table_lookup (sources->clients, source);
+  if (client_data)
+     client = E_CLIENT (g_object_ref (client_data->client));
+  g_mutex_unlock (&sources->clients_lock);
 
-  list = e_source_registry_list_sources (registry, extension_name);
-
-  for (link = list; link != NULL; link = g_list_next (link))
-    {
-      ESource *source = E_SOURCE (link->data);
-      ESourceSelectable *extension;
-      gboolean show_source;
+  if (client)
+    return client;
 
-      extension = e_source_get_extension (source, extension_name);
-      show_source = e_source_get_enabled (source) && e_source_selectable_get_selected (extension);
-
-      if (show_source)
-        create_client_for_source (source, source_data->source_type, source_data);
-    }
-
-  debug_dump_ecal_list (source_data->clients);
-
-  g_list_free_full (list, g_object_unref);
-}
+  client = e_cal_client_connect_sync (source, source_type, wait_for_connected_seconds, cancellable, error);
+  if (!client)
+    return NULL;
 
-static void
-calendar_sources_registry_source_changed_cb (ESourceRegistry *registry,
-                                             ESource         *source,
-                                             CalendarSources *sources)
-{
-  if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR))
+  g_mutex_lock (&sources->clients_lock);
+  client_data = g_hash_table_lookup (sources->clients, source);
+  if (client_data)
     {
-      CalendarSourceData *source_data;
-      ESourceSelectable *extension;
-      gboolean have_client;
-      gboolean show_source;
-
-      source_data = &sources->appointment_sources;
-      extension = e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR);
-      have_client = (g_hash_table_lookup (source_data->clients, source) != NULL);
-      show_source = e_source_get_enabled (source) && e_source_selectable_get_selected (extension);
-
-      if (!show_source && have_client)
-        {
-          g_hash_table_remove (source_data->clients, source);
-          g_signal_emit (sources, source_data->changed_signal, 0);
-        }
-      if (show_source && !have_client)
-        {
-          create_client_for_source (source, source_data->source_type, source_data);
-          g_signal_emit (sources, source_data->changed_signal, 0);
-        }
+      g_clear_object (&client);
+      client = E_CLIENT (g_object_ref (client_data->client));
     }
-
-  if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST))
+   else
     {
-      CalendarSourceData *source_data;
-      ESourceSelectable *extension;
-      gboolean have_client;
-      gboolean show_source;
-
-      source_data = &sources->task_sources;
-      extension = e_source_get_extension (source, E_SOURCE_EXTENSION_TASK_LIST);
-      have_client = (g_hash_table_lookup (source_data->clients, source) != NULL);
-      show_source = e_source_get_enabled (source) && e_source_selectable_get_selected (extension);
-
-      if (!show_source && have_client)
-        {
-          g_hash_table_remove (source_data->clients, source);
-          g_signal_emit (sources, source_data->changed_signal, 0);
-        }
-      if (show_source && !have_client)
-        {
-          create_client_for_source (source, source_data->source_type, source_data);
-          g_signal_emit (sources, source_data->changed_signal, 0);
-        }
+      client_data = g_slice_new0 (ClientData);
+      client_data->client = E_CAL_CLIENT (g_object_ref (client));
+      client_data->backend_died_id = g_signal_connect (client,
+                                                       "backend-died",
+                                                       G_CALLBACK (backend_died_cb),
+                                                       sources);
+
+      g_hash_table_insert (sources->clients, g_object_ref (source), client_data);
     }
+  g_mutex_unlock (&sources->clients_lock);
+
+  return client;
 }
 
+typedef struct _AsyncContext {
+  ESource *source;
+  ECalClientSourceType source_type;
+  guint32 wait_for_connected_seconds;
+} AsyncContext;
+
 static void
-calendar_sources_registry_source_removed_cb (ESourceRegistry *registry,
-                                             ESource         *source,
-                                             CalendarSources *sources)
+async_context_free (gpointer ptr)
 {
-  if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR))
-    {
-      CalendarSourceData *source_data;
+  AsyncContext *ctx = ptr;
 
-      source_data = &sources->appointment_sources;
-      g_hash_table_remove (source_data->clients, source);
-      g_signal_emit (sources, source_data->changed_signal, 0);
-    }
-
-  if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST))
+  if (ctx)
     {
-      CalendarSourceData *source_data;
-
-      source_data = &sources->task_sources;
-      g_hash_table_remove (source_data->clients, source);
-      g_signal_emit (sources, source_data->changed_signal, 0);
+      g_clear_object (&ctx->source);
+      g_slice_free (AsyncContext, ctx);
     }
 }
 
 static void
-ensure_appointment_sources (CalendarSources *sources)
+calendar_sources_connect_client_thread (GTask *task,
+                                        gpointer source_object,
+                                        gpointer task_data,
+                                        GCancellable *cancellable)
 {
-  if (!sources->appointment_sources.loaded)
+  CalendarSources *sources = source_object;
+  AsyncContext *ctx = task_data;
+  EClient *client;
+  GError *local_error = NULL;
+
+  client = calendar_sources_connect_client_sync (sources, ctx->source, ctx->source_type,
+                                                 ctx->wait_for_connected_seconds, cancellable, &local_error);
+  if (!client)
     {
-      calendar_sources_load_esource_list (sources->registry,
-                                          &sources->appointment_sources);
-      sources->appointment_sources.loaded = TRUE;
+      if (local_error)
+        g_task_return_error (task, local_error);
+      else
+        g_task_return_pointer (task, NULL, NULL);
+    } else {
+      g_task_return_pointer (task, client, g_object_unref);
     }
 }
 
-GList *
-calendar_sources_get_appointment_clients (CalendarSources *sources)
+void
+calendar_sources_connect_client (CalendarSources *sources,
+                                 ESource *source,
+                                 ECalClientSourceType source_type,
+                                 guint32 wait_for_connected_seconds,
+                                 GCancellable *cancellable,
+                                 GAsyncReadyCallback callback,
+                                 gpointer user_data)
 {
-  GList *list, *link;
+  AsyncContext *ctx;
+  g_autoptr (GTask) task = NULL;
 
-  g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), NULL);
+  ctx = g_slice_new0 (AsyncContext);
+  ctx->source = g_object_ref (source);
+  ctx->source_type = source_type;
+  ctx->wait_for_connected_seconds = wait_for_connected_seconds;
 
-  ensure_appointment_sources (sources);
+  task = g_task_new (sources, cancellable, callback, user_data);
+  g_task_set_source_tag (task, calendar_sources_connect_client);
+  g_task_set_task_data (task, ctx, async_context_free);
 
-  list = g_hash_table_get_values (sources->appointment_sources.clients);
+  g_task_run_in_thread (task, calendar_sources_connect_client_thread);
+}
 
-  for (link = list; link != NULL; link = g_list_next (link))
-    link->data = ((ClientData *) link->data)->client;
+EClient *
+calendar_sources_connect_client_finish (CalendarSources *sources,
+                                        GAsyncResult *result,
+                                        GError **error)
+{
+  g_return_val_if_fail (g_task_is_valid (result, sources), NULL);
+  g_return_val_if_fail (g_async_result_is_tagged (result, calendar_sources_connect_client), NULL);
 
-  return list;
+  return g_task_propagate_pointer (G_TASK (result), error);
 }
 
-static void
-ensure_task_sources (CalendarSources *sources)
+
+void
+print_debug (const gchar *format,
+             ...)
 {
-  if (!sources->task_sources.loaded)
+  g_autofree char *s = NULL;
+  g_autofree char *timestamp = NULL;
+  va_list ap;
+  g_autoptr (GDateTime) now = NULL;
+  static volatile gsize once_init_value = 0;
+  static gboolean show_debug = FALSE;
+  static guint pid = 0;
+
+  if (g_once_init_enter (&once_init_value))
     {
-      calendar_sources_load_esource_list (sources->registry,
-                                          &sources->task_sources);
-      sources->task_sources.loaded = TRUE;
+      show_debug = (g_getenv ("CALENDAR_SERVER_DEBUG") != NULL);
+      pid = getpid ();
+      g_once_init_leave (&once_init_value, 1);
     }
-}
-
-GList *
-calendar_sources_get_task_clients (CalendarSources *sources)
-{
-  GList *list, *link;
 
-  g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), NULL);
-
-  ensure_task_sources (sources);
-
-  list = g_hash_table_get_values (sources->task_sources.clients);
-
-  for (link = list; link != NULL; link = g_list_next (link))
-    link->data = ((ClientData *) link->data)->client;
-
-  return list;
-}
+  if (!show_debug)
+    goto out;
 
-gboolean
-calendar_sources_has_sources (CalendarSources *sources)
-{
-  g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), FALSE);
+  now = g_date_time_new_now_local ();
+  timestamp = g_date_time_format (now, "%H:%M:%S");
 
-  ensure_appointment_sources (sources);
-  ensure_task_sources (sources);
+  va_start (ap, format);
+  s = g_strdup_vprintf (format, ap);
+  va_end (ap);
 
-  return g_hash_table_size (sources->appointment_sources.clients) > 0 ||
-    g_hash_table_size (sources->task_sources.clients) > 0;
+  g_print ("gnome-shell-calendar-server[%d]: %s.%03d: %s\n",
+           pid, timestamp, g_date_time_get_microsecond (now), s);
+ out:
+  ;
 }
index d11850fff0878abc9716a5696a42fda1c00045b3..1ffc8ad01df814233940e013de4dcddd006be92b 100644 (file)
 
 #include <glib-object.h>
 
+#define EDS_DISABLE_DEPRECATED
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+#include <libedataserver/libedataserver.h>
+#include <libecal/libecal.h>
+G_GNUC_END_IGNORE_DEPRECATIONS
+
 G_BEGIN_DECLS
 
 #define CALENDAR_TYPE_SOURCES (calendar_sources_get_type ())
 G_DECLARE_FINAL_TYPE (CalendarSources, calendar_sources,
                       CALENDAR, SOURCES, GObject)
 
-CalendarSources *calendar_sources_get                     (void);
-GList           *calendar_sources_get_appointment_clients (CalendarSources *sources);
-GList           *calendar_sources_get_task_clients        (CalendarSources *sources);
+CalendarSources *calendar_sources_get                (void);
+ESourceRegistry *calendar_sources_get_registry       (CalendarSources *sources);
+GSList          *calendar_sources_ref_clients        (CalendarSources *sources);
+gboolean         calendar_sources_has_clients        (CalendarSources *sources);
+
+void             calendar_sources_connect_client     (CalendarSources *sources,
+                                                      ESource *source,
+                                                      ECalClientSourceType source_type,
+                                                      guint32 wait_for_connected_seconds,
+                                                      GCancellable *cancellable,
+                                                      GAsyncReadyCallback callback,
+                                                      gpointer user_data);
+EClient         *calendar_sources_connect_client_finish
+                                                     (CalendarSources *sources,
+                                                      GAsyncResult *result,
+                                                      GError **error);
 
-gboolean         calendar_sources_has_sources             (CalendarSources *sources);
+/* Set the environment variable CALENDAR_SERVER_DEBUG to show debug */
+void            print_debug                          (const gchar *str,
+                                                      ...) G_GNUC_PRINTF (1, 2);
 
 G_END_DECLS
 
index 23398bb0375d7ff6377e300f1dc07725b20b1bed..8ee8d3f1c58cad0634c66f694d21c26927736ee9 100644 (file)
@@ -41,21 +41,25 @@ G_GNUC_END_IGNORE_DEPRECATIONS
 
 #include "calendar-sources.h"
 
-/* Set the environment variable CALENDAR_SERVER_DEBUG to show debug */
-static void print_debug (const gchar *str, ...);
-
 #define BUS_NAME "org.gnome.Shell.CalendarServer"
 
 static const gchar introspection_xml[] =
   "<node>"
   "  <interface name='org.gnome.Shell.CalendarServer'>"
-  "    <method name='GetEvents'>"
+  "    <method name='SetTimeRange'>"
   "      <arg type='x' name='since' direction='in'/>"
   "      <arg type='x' name='until' direction='in'/>"
   "      <arg type='b' name='force_reload' direction='in'/>"
-  "      <arg type='a(sssbxxa{sv})' name='events' direction='out'/>"
   "    </method>"
-  "    <signal name='Changed'/>"
+  "    <signal name='EventsAddedOrUpdated'>"
+  "      <arg type='a(ssbxxa{sv})' name='events' direction='out'/>"
+  "    </signal>"
+  "    <signal name='EventsRemoved'>"
+  "      <arg type='as' name='ids' direction='out'/>"
+  "    </signal>"
+  "    <signal name='ClientDisappeared'>"
+  "      <arg type='s' name='source_uid' direction='out'/>"
+  "    </signal>"
   "    <property name='Since' type='x' access='read'/>"
   "    <property name='Until' type='x' access='read'/>"
   "    <property name='HasCalendars' type='b' access='read'/>"
@@ -75,35 +79,40 @@ static App *_global_app = NULL;
 
 /* ---------------------------------------------------------------------------------------------------- */
 
+/* While the UID is usually enough to identify an event,
+ * only the triple of (source,UID,RID) is fully unambiguous;
+ * neither may contain '\n', so we can safely use it to
+ * create a unique ID from the triple
+ */
+static gchar *
+create_event_id (const gchar *source_uid,
+                 const gchar *comp_uid,
+                 const gchar *comp_rid)
+{
+  return g_strconcat (
+    source_uid ? source_uid : "",
+    "\n",
+    comp_uid ? comp_uid : "",
+    "\n",
+    comp_rid ? comp_rid : "",
+    NULL);
+}
+
 typedef struct
 {
-  char *rid;
-  time_t start_time;
-  time_t end_time;
-} CalendarOccurrence;
+  ECalClient *client;
+  GSList **pappointments; /* CalendarAppointment * */
+} CollectAppointmentsData;
 
 typedef struct
 {
-  char   *uid;
-  char   *source_id;
-  char   *backend_name;
-  char   *summary;
-  char   *description;
-  char   *color_string;
+  gchar  *id;
+  gchar  *summary;
   time_t  start_time;
   time_t  end_time;
   guint   is_all_day : 1;
-
-  /* Only used internally */
-  GSList *occurrences;
 } CalendarAppointment;
 
-typedef struct
-{
-  ECalClient *client;
-  GHashTable *appointments;
-} CollectAppointmentsData;
-
 static time_t
 get_time_from_property (ECalClient            *cal,
                         ICalComponent         *icomp,
@@ -142,46 +151,6 @@ get_time_from_property (ECalClient            *cal,
   return retval;
 }
 
-static char *
-get_ical_uid (ICalComponent *icomp)
-{
-  return g_strdup (i_cal_component_get_uid (icomp));
-}
-
-static char *
-get_ical_summary (ICalComponent *icomp)
-{
-  ICalProperty *prop;
-  char         *retval;
-
-  prop = i_cal_component_get_first_property (icomp, I_CAL_SUMMARY_PROPERTY);
-  if (!prop)
-    return NULL;
-
-  retval = g_strdup (i_cal_property_get_summary (prop));
-
-  g_object_unref (prop);
-
-  return retval;
-}
-
-static char *
-get_ical_description (ICalComponent *icomp)
-{
-  ICalProperty *prop;
-  char         *retval;
-
-  prop = i_cal_component_get_first_property (icomp, I_CAL_DESCRIPTION_PROPERTY);
-  if (!prop)
-    return NULL;
-
-  retval = g_strdup (i_cal_property_get_description (prop));
-
-  g_object_unref (prop);
-
-  return retval;
-}
-
 static inline time_t
 get_ical_start_time (ECalClient    *cal,
                      ICalComponent *icomp,
@@ -275,172 +244,48 @@ get_ical_completed_time (ECalClient    *cal,
                                  default_zone);
 }
 
-static char *
-get_source_color (ECalClient *esource)
-{
-  ESource *source;
-  ECalClientSourceType source_type;
-  ESourceSelectable *extension;
-  const gchar *extension_name;
-
-  g_return_val_if_fail (E_IS_CAL_CLIENT (esource), NULL);
-
-  source = e_client_get_source (E_CLIENT (esource));
-  source_type = e_cal_client_get_source_type (esource);
-
-  switch (source_type)
-    {
-      case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
-        extension_name = E_SOURCE_EXTENSION_CALENDAR;
-        break;
-      case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
-        extension_name = E_SOURCE_EXTENSION_TASK_LIST;
-        break;
-      default:
-        g_return_val_if_reached (NULL);
-    }
-
-  extension = e_source_get_extension (source, extension_name);
-
-  return e_source_selectable_dup_color (extension);
-}
-
-static gchar *
-get_source_backend_name (ECalClient *esource)
+static CalendarAppointment *
+calendar_appointment_new (ECalClient    *cal,
+                          ECalComponent *comp)
 {
-  ESource *source;
-  ECalClientSourceType source_type;
-  ESourceBackend *extension;
-  const gchar *extension_name;
-
-  g_return_val_if_fail (E_IS_CAL_CLIENT (esource), NULL);
+  CalendarAppointment *appt;
+  ICalTimezone *default_zone;
+  ICalComponent *ical;
+  ECalComponentId *id;
 
-  source = e_client_get_source (E_CLIENT (esource));
-  source_type = e_cal_client_get_source_type (esource);
+  default_zone = e_cal_client_get_default_timezone (cal);
+  ical = e_cal_component_get_icalcomponent (comp);
+  id = e_cal_component_get_id (comp);
 
-  switch (source_type)
-    {
-      case E_CAL_CLIENT_SOURCE_TYPE_EVENTS:
-        extension_name = E_SOURCE_EXTENSION_CALENDAR;
-        break;
-      case E_CAL_CLIENT_SOURCE_TYPE_TASKS:
-        extension_name = E_SOURCE_EXTENSION_TASK_LIST;
-        break;
-      default:
-        g_return_val_if_reached (NULL);
-    }
+  appt = g_new0 (CalendarAppointment, 1);
 
-  extension = e_source_get_extension (source, extension_name);
+  appt->id          = create_event_id (e_source_get_uid (e_client_get_source (E_CLIENT (cal))),
+                                       id ? e_cal_component_id_get_uid (id) : NULL,
+                                       id ? e_cal_component_id_get_rid (id) : NULL);
+  appt->summary     = g_strdup (i_cal_component_get_summary (ical));
+  appt->start_time  = get_ical_start_time (cal, ical, default_zone);
+  appt->end_time    = get_ical_end_time (cal, ical, default_zone);
+  appt->is_all_day  = get_ical_is_all_day (cal,
+                                           ical,
+                                           appt->start_time,
+                                           default_zone);
 
-  return e_source_backend_dup_backend_name (extension);
-}
+  e_cal_component_id_free (id);
 
-static inline int
-null_safe_strcmp (const char *a,
-                  const char *b)
-{
-  return (!a && !b) ? 0 : (a && !b) || (!a && b) ? 1 : strcmp (a, b);
+  return appt;
 }
 
-static inline gboolean
-calendar_appointment_equal (CalendarAppointment *a,
-                            CalendarAppointment *b)
+static void
+calendar_appointment_free (gpointer ptr)
 {
-  GSList *la, *lb;
-
-  if (g_slist_length (a->occurrences) != g_slist_length (b->occurrences))
-      return FALSE;
+  CalendarAppointment *appt = ptr;
 
-  for (la = a->occurrences, lb = b->occurrences; la && lb; la = la->next, lb = lb->next)
+  if (appt)
     {
-      CalendarOccurrence *oa = la->data;
-      CalendarOccurrence *ob = lb->data;
-
-      if (oa->start_time != ob->start_time ||
-          oa->end_time   != ob->end_time ||
-          null_safe_strcmp (oa->rid, ob->rid) != 0)
-        return FALSE;
+      g_free (appt->id);
+      g_free (appt->summary);
+      g_free (appt);
     }
-
-  return
-    null_safe_strcmp (a->uid,          b->uid)          == 0 &&
-    null_safe_strcmp (a->source_id,    b->source_id)    == 0 &&
-    null_safe_strcmp (a->backend_name, b->backend_name) == 0 &&
-    null_safe_strcmp (a->summary,      b->summary)      == 0 &&
-    null_safe_strcmp (a->description,  b->description)  == 0 &&
-    null_safe_strcmp (a->color_string, b->color_string) == 0 &&
-    a->start_time == b->start_time                         &&
-    a->end_time   == b->end_time                           &&
-    a->is_all_day == b->is_all_day;
-}
-
-static void
-calendar_appointment_free (CalendarAppointment *appointment)
-{
-  GSList *l;
-
-  for (l = appointment->occurrences; l; l = l->next)
-    g_free (((CalendarOccurrence *)l->data)->rid);
-  g_slist_free_full (appointment->occurrences, g_free);
-  appointment->occurrences = NULL;
-
-  g_free (appointment->uid);
-  appointment->uid = NULL;
-
-  g_free (appointment->source_id);
-  appointment->source_id = NULL;
-
-  g_free (appointment->backend_name);
-  appointment->backend_name = NULL;
-
-  g_free (appointment->summary);
-  appointment->summary = NULL;
-
-  g_free (appointment->description);
-  appointment->description = NULL;
-
-  g_free (appointment->color_string);
-  appointment->color_string = NULL;
-
-  appointment->start_time = 0;
-  appointment->is_all_day = FALSE;
-}
-
-static void
-calendar_appointment_init (CalendarAppointment  *appointment,
-                           ICalComponent        *icomp,
-                           ECalClient           *cal)
-{
-  ICalTimezone *default_zone;
-  const char *source_id;
-
-  source_id = e_source_get_uid (e_client_get_source (E_CLIENT (cal)));
-  default_zone = e_cal_client_get_default_timezone (cal);
-
-  appointment->uid          = get_ical_uid (icomp);
-  appointment->source_id    = g_strdup (source_id);
-  appointment->backend_name = get_source_backend_name (cal);
-  appointment->summary      = get_ical_summary (icomp);
-  appointment->description  = get_ical_description (icomp);
-  appointment->color_string = get_source_color (cal);
-  appointment->start_time   = get_ical_start_time (cal, icomp, default_zone);
-  appointment->end_time     = get_ical_end_time (cal, icomp, default_zone);
-  appointment->is_all_day   = get_ical_is_all_day (cal,
-                                                   icomp,
-                                                   appointment->start_time,
-                                                   default_zone);
-}
-
-static CalendarAppointment *
-calendar_appointment_new (ICalComponent        *icomp,
-                          ECalClient           *cal)
-{
-  CalendarAppointment *appointment;
-
-  appointment = g_new0 (CalendarAppointment, 1);
-
-  calendar_appointment_init (appointment, icomp, cal);
-  return appointment;
 }
 
 static time_t
@@ -463,34 +308,25 @@ generate_instances_cb (ICalComponent *icomp,
                        GCancellable *cancellable,
                        GError **error)
 {
-  ECalClient *cal = ((CollectAppointmentsData *)user_data)->client;
-  GHashTable *appointments = ((CollectAppointmentsData *)user_data)->appointments;
+  CollectAppointmentsData *data = user_data;
   CalendarAppointment *appointment;
-  CalendarOccurrence *occurrence;
+  ECalComponent *comp;
   ICalTimezone *default_zone;
-  const gchar *uid;
 
-  default_zone = e_cal_client_get_default_timezone (cal);
-  uid = i_cal_component_get_uid (icomp);
-  appointment = g_hash_table_lookup (appointments, uid);
+  default_zone = e_cal_client_get_default_timezone (data->client);
+  comp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomp));
 
-  if (appointment == NULL)
-    {
-      appointment = calendar_appointment_new (icomp, cal);
-      g_hash_table_insert (appointments, g_strdup (uid), appointment);
-    }
+  appointment             = calendar_appointment_new (data->client, comp);
+  appointment->start_time = timet_from_ical_time (instance_start, default_zone);
+  appointment->end_time   = timet_from_ical_time (instance_end, default_zone);
 
-  occurrence             = g_new0 (CalendarOccurrence, 1);
-  occurrence->start_time = timet_from_ical_time (instance_start, default_zone);
-  occurrence->end_time   = timet_from_ical_time (instance_end, default_zone);
-  occurrence->rid        = e_cal_util_component_get_recurid_as_string (icomp);
+  *(data->pappointments) = g_slist_prepend (*(data->pappointments), appointment);
 
-  appointment->occurrences = g_slist_append (appointment->occurrences, occurrence);
+  g_clear_object (&comp);
 
   return TRUE;
 }
 
-
 /* ---------------------------------------------------------------------------------------------------- */
 
 struct _App
@@ -503,24 +339,23 @@ struct _App
   ICalTimezone *zone;
 
   CalendarSources *sources;
-  gulong sources_signal_id;
-
-  /* hash from uid to CalendarAppointment objects */
-  GHashTable *appointments;
+  gulong client_appeared_signal_id;
+  gulong client_disappeared_signal_id;
 
   gchar *timezone_location;
 
-  guint changed_timeout_id;
-
-  gboolean cache_invalid;
+  GSList *notify_appointments; /* CalendarAppointment *, for EventsAdded */
+  GSList *notify_ids; /* gchar *, for EventsRemoved */
+  guint events_added_timeout_id;
+  guint events_removed_timeout_id;
 
-  GList *live_views;
+  GSList *live_views;
 };
 
 static void
 app_update_timezone (App *app)
 {
-  gchar *location;
+  g_autofree char *location = NULL;
 
   location = e_cal_system_timezone_get_location ();
   if (g_strcmp0 (location, app->timezone_location) != 0)
@@ -530,74 +365,203 @@ app_update_timezone (App *app)
       else
         app->zone = i_cal_timezone_get_builtin_timezone (location);
       g_free (app->timezone_location);
-      app->timezone_location = location;
+      app->timezone_location = g_steal_pointer (&location);
       print_debug ("Using timezone %s", app->timezone_location);
     }
-  else
-    {
-      g_free (location);
-    }
 }
 
 static gboolean
-on_app_schedule_changed_cb (gpointer user_data)
+on_app_schedule_events_added_cb (gpointer user_data)
 {
   App *app = user_data;
-  print_debug ("Emitting changed");
+  GVariantBuilder builder, extras_builder;
+  GSList *events, *link;
+
+  if (g_source_is_destroyed (g_main_current_source ()))
+    return FALSE;
+
+  events = g_slist_reverse (app->notify_appointments);
+  app->notify_appointments = NULL;
+  app->events_added_timeout_id = 0;
+
+  print_debug ("Emitting EventsAddedOrUpdated with %d events", g_slist_length (events));
+
+  if (!events)
+    return FALSE;
+
+  /* The a{sv} is used as an escape hatch in case we want to provide more
+   * information in the future without breaking ABI
+   */
+  g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ssbxxa{sv})"));
+  for (link = events; link; link = g_slist_next (link))
+    {
+      CalendarAppointment *appt = link->data;
+      time_t start_time = appt->start_time;
+      time_t end_time   = appt->end_time;
+
+      if ((start_time >= app->since &&
+           start_time < app->until) ||
+          (start_time <= app->since &&
+          (end_time - 1) > app->since))
+        {
+          g_variant_builder_init (&extras_builder, G_VARIANT_TYPE ("a{sv}"));
+          g_variant_builder_add (&builder,
+                                 "(ssbxxa{sv})",
+                                 appt->id,
+                                 appt->summary != NULL ? appt->summary : "",
+                                 (gboolean) appt->is_all_day,
+                                 (gint64) start_time,
+                                 (gint64) end_time,
+                                 extras_builder);
+          g_variant_builder_clear (&extras_builder);
+        }
+    }
+
   g_dbus_connection_emit_signal (app->connection,
                                  NULL, /* destination_bus_name */
                                  "/org/gnome/Shell/CalendarServer",
                                  "org.gnome.Shell.CalendarServer",
-                                 "Changed",
-                                 NULL, /* no params */
+                                 "EventsAddedOrUpdated",
+                                 g_variant_new ("(a(ssbxxa{sv}))", &builder),
                                  NULL);
-  app->changed_timeout_id = 0;
+
+  g_variant_builder_clear (&builder);
+
+  g_slist_free_full (events, calendar_appointment_free);
+
   return FALSE;
 }
 
 static void
-app_schedule_changed (App *app)
+app_schedule_events_added (App *app)
 {
-  print_debug ("Scheduling changed");
-  if (app->changed_timeout_id == 0)
+  print_debug ("Scheduling EventsAddedOrUpdated");
+  if (app->events_added_timeout_id == 0)
     {
-      app->changed_timeout_id = g_timeout_add (2000,
-                                               on_app_schedule_changed_cb,
-                                               app);
-      g_source_set_name_by_id (app->changed_timeout_id, "[gnome-shell] on_app_schedule_changed_cb");
+      app->events_added_timeout_id = g_timeout_add_seconds (2,
+                                                            on_app_schedule_events_added_cb,
+                                                            app);
+      g_source_set_name_by_id (app->events_added_timeout_id, "[gnome-shell] on_app_schedule_events_added_cb");
     }
 }
 
+static gboolean
+on_app_schedule_events_removed_cb (gpointer user_data)
+{
+  App *app = user_data;
+  GVariantBuilder builder;
+  GSList *ids, *link;
+
+  if (g_source_is_destroyed (g_main_current_source ()))
+    return FALSE;
+
+  ids = app->notify_ids;
+  app->notify_ids = NULL;
+  app->events_removed_timeout_id = 0;
+
+  print_debug ("Emitting EventsRemoved with %d ids", g_slist_length (ids));
+
+  if (!ids)
+    return FALSE;
+
+  g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
+  for (link = ids; link; link = g_slist_next (link))
+    {
+      const gchar *id = link->data;
+
+      g_variant_builder_add (&builder, "s", id);
+    }
+
+  g_dbus_connection_emit_signal (app->connection,
+                                 NULL, /* destination_bus_name */
+                                 "/org/gnome/Shell/CalendarServer",
+                                 "org.gnome.Shell.CalendarServer",
+                                 "EventsRemoved",
+                                 g_variant_new ("(as)", &builder),
+                                 NULL);
+  g_variant_builder_clear (&builder);
+
+  g_slist_free_full (ids, g_free);
+
+  return FALSE;
+}
+
 static void
-invalidate_cache (App *app)
+app_schedule_events_removed (App *app)
 {
-  app->cache_invalid = TRUE;
+  print_debug ("Scheduling EventsRemoved");
+  if (app->events_removed_timeout_id == 0)
+    {
+      app->events_removed_timeout_id = g_timeout_add_seconds (2,
+                                                              on_app_schedule_events_removed_cb,
+                                                              app);
+      g_source_set_name_by_id (app->events_removed_timeout_id, "[gnome-shell] on_app_schedule_events_removed_cb");
+    }
 }
 
 static void
-on_objects_added (ECalClientView *view,
-                  GSList         *objects,
-                  gpointer        user_data)
+app_process_added_modified_objects (App *app,
+                                    ECalClientView *view,
+                                    GSList *objects) /* ICalComponent * */
 {
-  App *app = user_data;
-  GSList *l;
+  ECalClient *cal_client;
+  GSList *link;
+  gboolean expand_recurrences;
 
-  print_debug ("%s for calendar", G_STRFUNC);
+  cal_client = e_cal_client_view_ref_client (view);
+  expand_recurrences = e_cal_client_get_source_type (cal_client) == E_CAL_CLIENT_SOURCE_TYPE_EVENTS;
 
-  for (l = objects; l != NULL; l = l->next)
+  for (link = objects; link; link = g_slist_next (link))
     {
-      ICalComponent *icomp = l->data;
-      const char *uid;
+      ECalComponent *comp;
+      ICalComponent *icomp = link->data;
 
-      uid = i_cal_component_get_uid (icomp);
+      if (!icomp || !i_cal_component_get_uid (icomp))
+        continue;
 
-      if (g_hash_table_lookup (app->appointments, uid) == NULL)
+      if (expand_recurrences &&
+          !e_cal_util_component_is_instance (icomp) &&
+          e_cal_util_component_has_recurrences (icomp))
         {
-          /* new appointment we don't know about => changed signal */
-          invalidate_cache (app);
-          app_schedule_changed (app);
+          CollectAppointmentsData data;
+
+          data.client = cal_client;
+          data.pappointments = &app->notify_appointments;
+
+          e_cal_client_generate_instances_for_object_sync (cal_client, icomp, app->since, app->until, NULL,
+                                                           generate_instances_cb, &data);
+        }
+      else
+        {
+          comp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomp));
+          if (!comp)
+            continue;
+
+          app->notify_appointments = g_slist_prepend (app->notify_appointments,
+                                                      calendar_appointment_new (cal_client, comp));
+          g_object_unref (comp);
         }
     }
+
+  g_clear_object (&cal_client);
+
+  if (app->notify_appointments)
+    app_schedule_events_added (app);
+}
+
+static void
+on_objects_added (ECalClientView *view,
+                  GSList         *objects,
+                  gpointer        user_data)
+{
+  App *app = user_data;
+  ECalClient *client;
+
+  client = e_cal_client_view_ref_client (view);
+  print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (objects), e_source_get_uid (e_client_get_source (E_CLIENT (client))));
+  g_clear_object (&client);
+
+  app_process_added_modified_objects (app, view, objects);
 }
 
 static void
@@ -606,9 +570,13 @@ on_objects_modified (ECalClientView *view,
                      gpointer        user_data)
 {
   App *app = user_data;
-  print_debug ("%s for calendar", G_STRFUNC);
-  invalidate_cache (app);
-  app_schedule_changed (app);
+  ECalClient *client;
+
+  client = e_cal_client_view_ref_client (view);
+  print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (objects), e_source_get_uid (e_client_get_source (E_CLIENT (client))));
+  g_clear_object (&client);
+
+  app_process_added_modified_objects (app, view, objects);
 }
 
 static void
@@ -617,42 +585,60 @@ on_objects_removed (ECalClientView *view,
                     gpointer        user_data)
 {
   App *app = user_data;
-  print_debug ("%s for calendar", G_STRFUNC);
-  invalidate_cache (app);
-  app_schedule_changed (app);
-}
+  ECalClient *client;
+  GSList *link;
+  const gchar *source_uid;
 
-static void
-app_load_events (App *app)
-{
-  GList *clients;
-  GList *l;
-  GList *ll;
-  gchar *since_iso8601;
-  gchar *until_iso8601;
-  gchar *query;
-  const char *tz_location;
-
-  /* out with the old */
-  g_hash_table_remove_all (app->appointments);
-  /* nuke existing views */
-  for (ll = app->live_views; ll != NULL; ll = ll->next)
+  client = e_cal_client_view_ref_client (view);
+  source_uid = e_source_get_uid (e_client_get_source (E_CLIENT (client)));
+
+  print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (uids), source_uid);
+
+  for (link = uids; link; link = g_slist_next (link))
     {
-      ECalClientView *view = E_CAL_CLIENT_VIEW (ll->data);
-      g_signal_handlers_disconnect_by_func (view, on_objects_added, app);
-      g_signal_handlers_disconnect_by_func (view, on_objects_modified, app);
-      g_signal_handlers_disconnect_by_func (view, on_objects_removed, app);
-      e_cal_client_view_stop (view, NULL);
-      g_object_unref (view);
+      ECalComponentId *id = link->data;
+
+      if (!id)
+        continue;
+
+      app->notify_ids = g_slist_prepend (app->notify_ids,
+                                         create_event_id (source_uid,
+                                         e_cal_component_id_get_uid (id),
+                                         e_cal_component_id_get_rid (id)));
     }
-  g_list_free (app->live_views);
-  app->live_views = NULL;
+
+  g_clear_object (&client);
+
+  if (app->notify_ids)
+    app_schedule_events_removed (app);
+}
+
+static gboolean
+app_has_calendars (App *app)
+{
+  return app->live_views != NULL;
+}
+
+static ECalClientView *
+app_start_view (App *app,
+                ECalClient *cal_client)
+{
+  g_autofree char *since_iso8601 = NULL;
+  g_autofree char *until_iso8601 = NULL;
+  g_autofree char *query = NULL;
+  const gchar *tz_location;
+  ECalClientView *view = NULL;
+  g_autoptr (GError) error = NULL;
+
+  if (app->since <= 0 || app->since >= app->until)
+    return NULL;
 
   if (!app->since || !app->until)
     {
       print_debug ("Skipping load of events, no time interval set yet");
-      return;
+      return NULL;
     }
+
   /* timezone could have changed */
   app_update_timezone (app);
 
@@ -660,9 +646,10 @@ app_load_events (App *app)
   until_iso8601 = isodate_from_time_t (app->until);
   tz_location = i_cal_timezone_get_location (app->zone);
 
-  print_debug ("Loading events since %s until %s",
+  print_debug ("Loading events since %s until %s for calendar '%s'",
                since_iso8601,
-               until_iso8601);
+               until_iso8601,
+               e_source_get_uid (e_client_get_source (E_CLIENT (cal_client))));
 
   query = g_strdup_printf ("occur-in-time-range? (make-time \"%s\") "
                            "(make-time \"%s\") \"%s\"",
@@ -670,94 +657,186 @@ app_load_events (App *app)
                            until_iso8601,
                            tz_location);
 
-  clients = calendar_sources_get_appointment_clients (app->sources);
-  for (l = clients; l != NULL; l = l->next)
+  e_cal_client_set_default_timezone (cal_client, app->zone);
+
+  if (!e_cal_client_get_view_sync (cal_client, query, &view, NULL /* cancellable */, &error))
+    {
+      g_warning ("Error setting up live-query '%s' on calendar: %s\n", query, error ? error->message : "Unknown error");
+      view = NULL;
+    }
+  else
+    {
+      g_signal_connect (view,
+                        "objects-added",
+                        G_CALLBACK (on_objects_added),
+                        app);
+      g_signal_connect (view,
+                        "objects-modified",
+                        G_CALLBACK (on_objects_modified),
+                        app);
+      g_signal_connect (view,
+                        "objects-removed",
+                        G_CALLBACK (on_objects_removed),
+                        app);
+      e_cal_client_view_start (view, NULL);
+    }
+
+  return view;
+}
+
+static void
+app_stop_view (App *app,
+               ECalClientView *view)
+{
+      e_cal_client_view_stop (view, NULL);
+
+      g_signal_handlers_disconnect_by_func (view, on_objects_added, app);
+      g_signal_handlers_disconnect_by_func (view, on_objects_modified, app);
+      g_signal_handlers_disconnect_by_func (view, on_objects_removed, app);
+}
+
+static void
+app_update_views (App *app)
+{
+  GSList *link, *clients;
+
+  for (link = app->live_views; link; link = g_slist_next (link))
     {
-      ECalClient *cal = E_CAL_CLIENT (l->data);
-      GError *error;
+      app_stop_view (app, link->data);
+    }
+
+  g_slist_free_full (app->live_views, g_object_unref);
+  app->live_views = NULL;
+
+  clients = calendar_sources_ref_clients (app->sources);
+
+  for (link = clients; link; link = g_slist_next (link))
+    {
+      ECalClient *cal_client = link->data;
       ECalClientView *view;
-      CollectAppointmentsData data;
-
-      e_cal_client_set_default_timezone (cal, app->zone);
-
-      data.client = cal;
-      data.appointments = app->appointments;
-      e_cal_client_generate_instances_sync (cal,
-                                            app->since,
-                                            app->until,
-                                            NULL,
-                                            generate_instances_cb,
-                                            &data);
-
-      error = NULL;
-      if (!e_cal_client_get_view_sync (cal,
-                                       query,
-                                       &view,
-                                       NULL, /* cancellable */
-                                       &error))
-        {
-          g_warning ("Error setting up live-query on calendar: %s\n", error->message);
-          g_error_free (error);
-        }
-      else
-        {
-          g_signal_connect (view,
-                            "objects-added",
-                            G_CALLBACK (on_objects_added),
-                            app);
-          g_signal_connect (view,
-                            "objects-modified",
-                            G_CALLBACK (on_objects_modified),
-                            app);
-          g_signal_connect (view,
-                            "objects-removed",
-                            G_CALLBACK (on_objects_removed),
-                            app);
-          e_cal_client_view_start (view, NULL);
-          app->live_views = g_list_prepend (app->live_views, view);
-        }
+
+      if (!cal_client)
+        continue;
+
+      view = app_start_view (app, cal_client);
+      if (view)
+        app->live_views = g_slist_prepend (app->live_views, view);
     }
-  g_list_free (clients);
-  g_free (since_iso8601);
-  g_free (until_iso8601);
-  g_free (query);
-  app->cache_invalid = FALSE;
+
+  g_slist_free_full (clients, g_object_unref);
 }
 
-static gboolean
-app_has_calendars (App *app)
+static void
+app_notify_has_calendars (App *app)
 {
-  return calendar_sources_has_sources (app->sources);
+  GVariantBuilder dict_builder;
+
+  g_variant_builder_init (&dict_builder, G_VARIANT_TYPE ("a{sv}"));
+  g_variant_builder_add (&dict_builder, "{sv}", "HasCalendars",
+                         g_variant_new_boolean (app_has_calendars (app)));
+
+  g_dbus_connection_emit_signal (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL),
+                                 NULL,
+                                 "/org/gnome/Shell/CalendarServer",
+                                 "org.freedesktop.DBus.Properties",
+                                 "PropertiesChanged",
+                                 g_variant_new ("(sa{sv}as)",
+                                                "org.gnome.Shell.CalendarServer",
+                                                &dict_builder,
+                                                NULL),
+                                 NULL);
+  g_variant_builder_clear (&dict_builder);
 }
 
 static void
-on_appointment_sources_changed (CalendarSources *sources,
-                                gpointer         user_data)
+on_client_appeared_cb (CalendarSources *sources,
+                       ECalClient *client,
+                       gpointer user_data)
 {
   App *app = user_data;
+  ECalClientView *view;
+  GSList *link;
+  const gchar *source_uid;
+
+  source_uid = e_source_get_uid (e_client_get_source (E_CLIENT (client)));
+
+  print_debug ("Client appeared '%s'", source_uid);
+
+  for (link = app->live_views; link; link = g_slist_next (link))
+    {
+      ECalClientView *view = link->data;
+      ECalClient *cal_client;
+      ESource *source;
+
+      cal_client = e_cal_client_view_ref_client (view);
+      source = e_client_get_source (E_CLIENT (cal_client));
+
+      if (g_strcmp0 (source_uid, e_source_get_uid (source)) == 0)
+        {
+          g_clear_object (&cal_client);
+          return;
+        }
+
+      g_clear_object (&cal_client);
+    }
+
+  view = app_start_view (app, client);
+
+  if (view)
+    {
+      app->live_views = g_slist_prepend (app->live_views, view);
 
-  print_debug ("Sources changed\n");
-  app_load_events (app);
-
-  /* Notify the HasCalendars property */
-  {
-    GVariantBuilder dict_builder;
-
-    g_variant_builder_init (&dict_builder, G_VARIANT_TYPE ("a{sv}"));
-    g_variant_builder_add (&dict_builder, "{sv}", "HasCalendars",
-                           g_variant_new_boolean (app_has_calendars (app)));
-
-    g_dbus_connection_emit_signal (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL),
-                                   NULL,
-                                   "/org/gnome/Shell/CalendarServer",
-                                   "org.freedesktop.DBus.Properties",
-                                   "PropertiesChanged",
-                                   g_variant_new ("(sa{sv}as)",
-                                                  "org.gnome.Shell.CalendarServer",
-                                                  &dict_builder,
-                                                  NULL),
-                                   NULL);
-  }
+      /* It's the first view, notify that it has calendars now */
+      if (!g_slist_next (app->live_views))
+        app_notify_has_calendars (app);
+    }
+}
+
+static void
+on_client_disappeared_cb (CalendarSources *sources,
+                          const gchar *source_uid,
+                          gpointer user_data)
+{
+  App *app = user_data;
+  GSList *link;
+
+  print_debug ("Client disappeared '%s'", source_uid);
+
+  for (link = app->live_views; link; link = g_slist_next (link))
+    {
+      ECalClientView *view = link->data;
+      ECalClient *cal_client;
+      ESource *source;
+
+      cal_client = e_cal_client_view_ref_client (view);
+      source = e_client_get_source (E_CLIENT (cal_client));
+
+      if (g_strcmp0 (source_uid, e_source_get_uid (source)) == 0)
+        {
+          g_clear_object (&cal_client);
+          app_stop_view (app, view);
+          app->live_views = g_slist_remove (app->live_views, view);
+          g_object_unref (view);
+
+          print_debug ("Emitting ClientDisappeared for '%s'", source_uid);
+
+          g_dbus_connection_emit_signal (app->connection,
+                                         NULL, /* destination_bus_name */
+                                         "/org/gnome/Shell/CalendarServer",
+                                         "org.gnome.Shell.CalendarServer",
+                                         "ClientDisappeared",
+                                         g_variant_new ("(s)", source_uid),
+                                         NULL);
+
+          /* It was the last view, notify that it doesn't have calendars now */
+          if (!app->live_views)
+            app_notify_has_calendars (app);
+
+          break;
+        }
+
+      g_clear_object (&cal_client);
+    }
 }
 
 static App *
@@ -768,15 +847,14 @@ app_new (GDBusConnection *connection)
   app = g_new0 (App, 1);
   app->connection = g_object_ref (connection);
   app->sources = calendar_sources_get ();
-  app->sources_signal_id = g_signal_connect (app->sources,
-                                             "appointment-sources-changed",
-                                             G_CALLBACK (on_appointment_sources_changed),
-                                             app);
-
-  app->appointments = g_hash_table_new_full (g_str_hash,
-                                             g_str_equal,
-                                             g_free,
-                                             (GDestroyNotify) calendar_appointment_free);
+  app->client_appeared_signal_id = g_signal_connect (app->sources,
+                                                     "client-appeared",
+                                                     G_CALLBACK (on_client_appeared_cb),
+                                                     app);
+  app->client_disappeared_signal_id = g_signal_connect (app->sources,
+                                                        "client-disappeared",
+                                                        G_CALLBACK (on_client_disappeared_cb),
+                                                        app);
 
   app_update_timezone (app);
 
@@ -786,28 +864,32 @@ app_new (GDBusConnection *connection)
 static void
 app_free (App *app)
 {
-  GList *ll;
-  for (ll = app->live_views; ll != NULL; ll = ll->next)
+  GSList *ll;
+
+  g_clear_handle_id (&app->events_added_timeout_id, g_source_remove);
+  g_clear_handle_id (&app->events_removed_timeout_id, g_source_remove);
+
+  for (ll = app->live_views; ll != NULL; ll = g_slist_next (ll))
     {
       ECalClientView *view = E_CAL_CLIENT_VIEW (ll->data);
-      g_signal_handlers_disconnect_by_func (view, on_objects_added, app);
-      g_signal_handlers_disconnect_by_func (view, on_objects_modified, app);
-      g_signal_handlers_disconnect_by_func (view, on_objects_removed, app);
-      e_cal_client_view_stop (view, NULL);
-      g_object_unref (view);
+
+      app_stop_view (app, view);
     }
-  g_list_free (app->live_views);
+
+  g_signal_handler_disconnect (app->sources,
+                               app->client_appeared_signal_id);
+  g_signal_handler_disconnect (app->sources,
+                               app->client_disappeared_signal_id);
 
   g_free (app->timezone_location);
 
-  g_hash_table_unref (app->appointments);
+  g_slist_free_full (app->live_views, g_object_unref);
+  g_slist_free_full (app->notify_appointments, calendar_appointment_free);
+  g_slist_free_full (app->notify_ids, g_free);
 
   g_object_unref (app->connection);
-  g_clear_signal_handler (&app->sources_signal_id, app->sources);
   g_object_unref (app->sources);
 
-  g_clear_handle_id (&app->changed_timeout_id, g_source_remove);
-
   g_free (app);
 }
 
@@ -825,15 +907,12 @@ handle_method_call (GDBusConnection       *connection,
 {
   App *app = user_data;
 
-  if (g_strcmp0 (method_name, "GetEvents") == 0)
+  if (g_strcmp0 (method_name, "SetTimeRange") == 0)
     {
-      GVariantBuilder builder;
-      GHashTableIter hash_iter;
-      CalendarAppointment *a;
       gint64 since;
       gint64 until;
-      gboolean force_reload;
-      gboolean window_changed;
+      gboolean force_reload = FALSE;
+      gboolean window_changed = FALSE;
 
       g_variant_get (parameters,
                      "(xxb)",
@@ -849,13 +928,12 @@ handle_method_call (GDBusConnection       *connection,
           goto out;
         }
 
-      print_debug ("Handling GetEvents (since=%" G_GINT64_FORMAT ", until=%" G_GINT64_FORMAT ", force_reload=%s)",
+      print_debug ("Handling SetTimeRange (since=%" G_GINT64_FORMAT ", until=%" G_GINT64_FORMAT ", force_reload=%s)",
                    since,
                    until,
                    force_reload ? "true" : "false");
 
-      window_changed = FALSE;
-      if (!(app->until == until && app->since == since))
+      if (app->until != until || app->since != since)
         {
           GVariantBuilder *builder;
           GVariantBuilder *invalidated_builder;
@@ -880,61 +958,15 @@ handle_method_call (GDBusConnection       *connection,
                                                         builder,
                                                         invalidated_builder),
                                          NULL); /* GError** */
-        }
 
-      /* reload events if necessary */
-      if (window_changed || force_reload || app->cache_invalid)
-        {
-          app_load_events (app);
+          g_variant_builder_unref (builder);
+          g_variant_builder_unref (invalidated_builder);
         }
 
-      /* The a{sv} is used as an escape hatch in case we want to provide more
-       * information in the future without breaking ABI
-       */
-      g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(sssbxxa{sv})"));
-      g_hash_table_iter_init (&hash_iter, app->appointments);
-      while (g_hash_table_iter_next (&hash_iter, NULL, (gpointer) &a))
-        {
-          GVariantBuilder extras_builder;
-          GSList *l;
-
-          for (l = a->occurrences; l; l = l->next)
-            {
-              CalendarOccurrence *o = l->data;
-              time_t start_time = o->start_time;
-              time_t end_time   = o->end_time;
-
-              if ((start_time >= app->since &&
-                   start_time < app->until) ||
-                  (start_time <= app->since &&
-                  (end_time - 1) > app->since))
-                {
-                  /* While the UID is usually enough to identify an event,
-                   * only the triple of (source,UID,RID) is fully unambiguous;
-                   * neither may contain '\n', so we can safely use it to
-                   * create a unique ID from the triple
-                   */
-                  char *id = g_strdup_printf ("%s\n%s\n%s",
-                                              a->source_id,
-                                              a->uid,
-                                              o->rid ? o->rid : "");
-
-                  g_variant_builder_init (&extras_builder, G_VARIANT_TYPE ("a{sv}"));
-                  g_variant_builder_add (&builder,
-                                         "(sssbxxa{sv})",
-                                         id,
-                                         a->summary != NULL ? a->summary : "",
-                                         a->description != NULL ? a->description : "",
-                                         (gboolean) a->is_all_day,
-                                         (gint64) start_time,
-                                         (gint64) end_time,
-                                         extras_builder);
-                  g_free (id);
-                }
-            }
-        }
-      g_dbus_method_invocation_return_value (invocation,
-                                             g_variant_new ("(a(sssbxxa{sv}))", &builder));
+      g_dbus_method_invocation_return_value (invocation, NULL);
+
+      if (window_changed || force_reload)
+        app_update_views (app);
     }
   else
     {
@@ -989,12 +1021,12 @@ on_bus_acquired (GDBusConnection *connection,
                  const gchar     *name,
                  gpointer         user_data)
 {
-  GError *error;
+  GMainLoop *main_loop = user_data;
   guint registration_id;
+  g_autoptr (GError) error = NULL;
 
   _global_app = app_new (connection);
 
-  error = NULL;
   registration_id = g_dbus_connection_register_object (connection,
                                                        "/org/gnome/Shell/CalendarServer",
                                                        introspection_data->interfaces[0],
@@ -1004,16 +1036,15 @@ on_bus_acquired (GDBusConnection *connection,
                                                        &error);
   if (registration_id == 0)
     {
-      g_printerr ("Error exporting object: %s (%s %d)",
+      g_printerr ("Error exporting object: %s (%s %d)\n",
                   error->message,
                   g_quark_to_string (error->domain),
                   error->code);
-      g_error_free (error);
-      _exit (1);
+      g_main_loop_quit (main_loop);
+      return;
     }
 
   print_debug ("Connected to the session bus");
-
 }
 
 static void
@@ -1060,7 +1091,7 @@ int
 main (int    argc,
       char **argv)
 {
-  GError *error;
+  g_autoptr (GError) error = NULL;
   GOptionContext *opt_context;
   GMainLoop *main_loop;
   gint ret;
@@ -1077,11 +1108,9 @@ main (int    argc,
 
   opt_context = g_option_context_new ("gnome-shell calendar server");
   g_option_context_add_main_entries (opt_context, opt_entries, NULL);
-  error = NULL;
   if (!g_option_context_parse (opt_context, &argc, &argv, &error))
     {
-      g_printerr ("Error parsing options: %s", error->message);
-      g_error_free (error);
+      g_printerr ("Error parsing options: %s\n", error->message);
       goto out;
     }
 
@@ -1120,41 +1149,6 @@ main (int    argc,
     g_bus_unown_name (name_owner_id);
   if (opt_context != NULL)
     g_option_context_free (opt_context);
-  return ret;
-}
-
-/* ---------------------------------------------------------------------------------------------------- */
-
-static void __attribute__((format(printf, 1, 0)))
-print_debug (const gchar *format, ...)
-{
-  g_autofree char *s = NULL;
-  g_autofree char *timestamp = NULL;
-  va_list ap;
-  g_autoptr (GDateTime) now = NULL;
-  static volatile gsize once_init_value = 0;
-  static gboolean show_debug = FALSE;
-  static guint pid = 0;
-
-  if (g_once_init_enter (&once_init_value))
-    {
-      show_debug = (g_getenv ("CALENDAR_SERVER_DEBUG") != NULL);
-      pid = getpid ();
-      g_once_init_leave (&once_init_value, 1);
-    }
-
-  if (!show_debug)
-    goto out;
-
-  now = g_date_time_new_now_local ();
-  timestamp = g_date_time_format (now, "%H:%M:%S");
 
-  va_start (ap, format);
-  s = g_strdup_vprintf (format, ap);
-  va_end (ap);
-
-  g_print ("gnome-shell-calendar-server[%d]: %s.%03d: %s\n",
-           pid, timestamp, g_date_time_get_microsecond (now), s);
- out:
-  ;
+  return ret;
 }